Document number:
Date: 2012-09-19
Project: Programming Language C++, Library Working Group
Reply-to: Aurelian Melinte <ame01 at gmx dot net>

Call Stack Utilities and std::exception Extension Proposal

I. Table of Contents



II. Introduction

This is a two part proposal to: 

III. Motivation and Scope

To diagnose software defects, it is often necessary to have the call stack information and to be able to print it in a human-readable form.  Such information is valuable and would considerably ease error diagnostic, in particular for rare-to-occur software conditions.

The call stack is currently available to the C++ programmer only if resorting to platform-specific API.  To resolve an address to symbol information (function name, source file, line number, etc.) the programmer has to resort again  to platform-specific API. 

The call stack of the exact place where a software error has been detected and where an exception has been thrown is not currently available at the place where exception is caught. 

The proposal attempts to standardize access to the call stack information.

IV. Impact On the Standard

Additions

The proposal adds four new classes to standardize call stack information, for part a) of the proposal:

  1. A raw call stack container. 
  2. Two tools to resolve call stack addresses to symbol information. Implementations could provide more such resolvers.
  3. A tool to combine a call stack container with an address resolver for "cooked" call stack information.

Dependencies

The proposed functionality uses existing standard containers to keep the raw call stack and the symbol information. The choice of the containers is platform-dependent (in practice std::array proved to be a good choice for GNU/Linux).  It makes usage of platform specific APIs to extract the call stack and to resolve symbols but these APIs  should not add any new link dependencies.

Changes to current standard components

  1. A one method extension to std::exception. Only if part b) of the proposal is adopted.

V. Design Decisions

The proposed set of utilities does:


VI. Technical Specifications

call_stack

This class contains the raw call stack information and it offers an interface to access this information in a platform-independent way.  

call_stack:

call_stack exposes a standard container interface [1] to the call stack. However, part of the standard interface is missing, as it makes little sense to allow mutator operations to the call stack. Only read-only access is provided to the call stack information, with the exception of the swap() method.

The call stack information is accessed through a bidirectional const_iterator, as well as through a const subscript operator.

typedef platform-specific   raw_frame_type;       // void*
typedef platform-specific   const_raw_frame_type; // const void*

template < std::size_t MaxDepth> class call_stack
class call_stack
{
public:

    typedef raw_frame_type            value_type;
    typedef platform-specific         size_type;
    typedef platform-specific         difference_type;
    typedef const_raw_frame_type&     const_reference;         // no reference
    typedef const_raw_frame_type*     const_pointer;           // no pointer
    typedef platform-specific         const_iterator;          // no iterator
    typedef platform-specific         const_reverse_iterator;  // no reverse_iterator

    /**
     *  Get the call stack information upon instantiation
     *  if @param capture is true.
     */
    call_stack(bool capture = false)          noexcept;

    call_stack(call_stack&& other)            noexcept;
    call_stack(const call_stack& other)       noexcept;
    call_stack& operator=(call_stack&& other) noexcept;
    call_stack& operator=(call_stack other)   noexcept;
   
~call_stack()                             noexcept;

    size_type depth()      const noexcept;
    size_type max_depth()  const noexcept;

    size_type size()       const noexcept { return depth(); }
    size_type max_size()   const noexcept { return max_depth(); }

    bool empty()           const noexcept { return depth() == 0; }

    /*
     * const_iterator allows access to the const_raw_frame_type
     * constituting the call stack information.
     */
    const_iterator begin()  const;
    const_iterator end()    const;
    const_iterator cbegin() const;
    const_iterator cend()   const;

    const_reverse_iterator crbegin() const;
    const_reverse_iterator crend()   const;
    const_reverse_iterator rbegin()  const;
    const_reverse_iterator rend()    const;

    const_reference operator [] (size_type idx) const;
    const_reference at(size_type idx) const;

    bool operator ==(const call_stack& other) const; // no <, <=, >, >=
    bool operator !=(const call_stack& other) const;

    void swap(call_stack& other) noexcept;
}; //call_stack

symbol_info

symbol_info resolves a given address to human-readable symbol information and offers facilities for outputting the information to streams. 

The resolution is using the API offered by default by the platform that does not add link dependencies. For instance, for gcc-based platforms, symbol_info would use the glibc API. However, libraries such as libbfd (The Binary File Descriptor Library) [2] do offer better resolution (source file, line number) than glibc, but such libraries are independent of gcc and might have not been installed on a given machine. On GNU/Linux a libbfd_symbol_info can be written and used whenever libbfd is available, at the price of adding a dependency on a library that might not be installed by default. The choice to use a different symbol resolver is left to the programmer.

In the process of resolving, symbol_info can allocate dynamic storage as needed. This is undesirable in the context of an exception such as std::bad_alloc being caught, so a simpler version, symbol_info_base, with the same interface should exists. symbol_info_base is not allowed to allocate dynamic storage and consequently would not be able to resolve much symbol information, but it would allow a very basic call stack information to be printed. symbol_info_base can only use platform-specific symbol resolution APIs that do not allocate dynamic storage.  At a minimum,  symbol_info_base will only offer the call stack frame address. 

Again, only read-only access is provided to the frame information, with the exception of the swap() and resolve() methods. No method is allowed to throw.

The resolve() method allows for reuse of a constructed symbol_info object, for example by an iterator that iterates over a call_stack and is the only mutator aside swap().

class symbol_info //symbol_info_base
{
public:

    symbol_info(const_raw_frame_type addr)      noexcept;

    symbol_info(symbol_info const& other)       noexcept;
    symbol_info& operator=(symbol_info other)   noexcept;

    symbol_info(symbol_info&& other)            noexcept;
    symbol_info& operator=(symbol_info&& other) noexcept;

    void resolve(const_raw_frame_type addr)     noexcept;
    void swap(symbol_info& other)               noexcept;


    // The address the symbol info is for.
    const_raw_frame_type   addr()                    const
noexcept;
    const char*            binary_file()             const noexcept;
    const char*            raw_function_name()       const noexcept;
    const char*            demangled_function_name() const noexcept;
    char                   delta_sign()              const noexcept;
    long                   delta()                   const noexcept;
    const char*            source_file()             const noexcept;
    unsigned int           line_number()             const noexcept;


    friend inline std::ostream& operator<<(std::ostream& os,
                                           const symbol_info& frm)
    {
        os << "[" << std::hex << frm.addr() << "] "
           << frm.demangled_function_name()
           << " (" << frm.binary_file() << frm.delta_sign() << "0x" << std::hex << frm.delta() << ")"
           << " in " << frm.source_file() << ":" << std::dec << frm.line_number()
           ;
        return os;
    }
}; //symbol_info


call_stack_info

call_stack_info binds together a call_stack with a symbol resolver.  call_stack_info offers human-readable information to the call stack and offers facilities for outputting the information to streams. 

Only read-only access is offered by call_stack_info.  A bidirectional const_iterator  provides read-only frame-by-frame access to the stack information, as well as a const subscript operator. Both allow to resolve call stack frames to symbol information using the AddrResolver template parameter.

template < typename CallStack                      //= call_stack<40u>
         , typename AddrResolver                   = symbol_info
         >
class call_stack_info
{
public:

    typedef CallStack              stack_type;
    typedef AddrResolver           symbol_info_type;

    call_stack_info()                                   noexcept
    call_stack_info(const stack_type& stack)            noexcept;

    call_stack_info(call_stack_info&& other)            noexcept;
    call_stack_info& operator=(call_stack_info&& other) noexcept;
    call_stack_info(const call_stack_info& other)       noexcept;
    call_stack_info& operator=(call_stack_info other)   noexcept;

    void swap(call_stack_info& other) noexcept;

    friend inline std::ostream& operator<<(std::ostream& os,
                                           const call_stack_info& stk)
    {
        for (const auto& frm : stk._stack)
        {
            AddrResolver frmInfo(frm);
            os << frmInfo << "\n";
        }
        os << std::flush;
        return os;
    }

    std::string as_string() const;

    class const_iterator
            : public std::iterator< std::bidirectional_iterator_tag
                                  , ptrdiff_t
                                  >
    {
    public:

        const_iterator(const typename stack_type::const_iterator& it);

        bool operator==(const const_iterator& other) const;
        bool operator!=(const const_iterator& x)     const;

        const symbol_info_type& operator*()  const;
        const symbol_info_type* operator->() const;

        const_iterator& operator++();
        const_iterator operator++(int);

        const_iterator& operator--();
        const_iterator operator--(int);
    }; //const_iterator

    const_iterator begin()  const;
    const_iterator cbegin() const;
    const_iterator end()    const;
    const_iterator cend()   const;

private:

    stack_type         _stack;
};

std::exception

std::exception is extended to capture the call stack when it is instantiated.  It offers access to the call stack through its where() method: 

class std::exception
{
public:

    typedef std::call_stack< default_stack_depth/*40*/ > stack_type;

   
exception() noexcept : _where(true) {...}

    const stack_type& where() const noexcept
    {
        return _where;
    }

private:

    stack_type  _where;
};

Example Usage

Example 1:

    typedef call_stack<40> stack_type;

    stack_type; here(true);                   // Get the call stack
    std::cout << here.depth() << " frames:\n"
              << call_stack_info< stack_type, symbol_info >(here) << std::endl;

Example 2: usage within the context of an exception:

    try
    {
    ...
    }
    catch (std::exception& ex)
    {
        std::cerr << ex.what() << std::endl;
        std::cerr << "\nAt: \n"
                  << lpt::stack::call_stack_info< stack_type, symbol_info_base >(ex.where()) << std::endl;
        std::cerr << "\nAt: \n"
                  << lpt::stack::call_stack_info< stack_type, symbol_info >(ex.where()) << std::endl;
    }


VII. Existing Implementation

VIII. References


IX. Revision History